package org.visionlibrary.image.geomtric.lines.search;

import java.util.ArrayList;
import java.util.List;

import javax.media.jai.TiledImage;

import org.visionlibrary.image.geomtric.lines.search.model.AbstractSegmentFinder;
import org.visionlibrary.image.geomtric.model.Mask2d;
import org.visionlibrary.image.geomtric.model.Point2d;
import org.visionlibrary.image.geomtric.model.Vector2d;


/**
 * Klasa realizujaca algorytm wektoryzacji pracujacy poprzez sprawdzanie najblizszego
 * sasiedztwa wybranego punktu, jego dzialanie opisane jest w pracy pisemnej.
 */
public class NeighbourSegmentFinder extends AbstractSegmentFinder {
	public static final int[] DEFAULT_BACKGROUND_COLOR = { 0 };
	public static final double DEFAULT_ACCEPTABLE_COSINUS_ANGLE = 0.5d;
	public static final double DEFAULT_ACCEPTABLE_DISTANCE = 0.8d;
	public static final boolean DEFAULT_CHECK_USING_DOT_PRODUCT = true;
	public static final boolean DEFAULT_CHECK_USING_DISTANCE = true;

	private TiledImage image;
	private Mask2d beginingPoints;

	private int xLength, yLength;
	private List<Vector2d> lines;
	private double acceptableCosinusAngle;
	private double acceptableDistance;
	private boolean checkUsingDotProduct;
	private boolean checkUsingDistance;
	private int[] backgroundColor;

	public NeighbourSegmentFinder(double cosinusAngle, double distance, boolean useDotProduct, boolean useDistance, int[] bgColor) {
		this.acceptableCosinusAngle = cosinusAngle;
		this.acceptableDistance = distance;
		this.backgroundColor = bgColor;
		this.checkUsingDistance = useDistance;
		this.checkUsingDotProduct = useDotProduct;
	}
	
	public NeighbourSegmentFinder() {
		this.acceptableCosinusAngle = DEFAULT_ACCEPTABLE_COSINUS_ANGLE;
		this.acceptableDistance = DEFAULT_ACCEPTABLE_DISTANCE;
		this.checkUsingDotProduct = DEFAULT_CHECK_USING_DOT_PRODUCT;
		this.checkUsingDistance = DEFAULT_CHECK_USING_DISTANCE;
		this.backgroundColor = DEFAULT_BACKGROUND_COLOR;
	}

	@Override
	protected List<Vector2d> find(TiledImage image) {
		this.image = image;

		lines = new ArrayList<Vector2d>();
		xLength = image.getWidth();
		yLength = image.getHeight();

		//do sprawdzenia czy nie da sie wrzucic merge
		beginingPoints = Mask2d.createForegroundMask(image, backgroundColor);

		for (int x = 0; x < xLength; ++x) {
			for (int y = 0; y < yLength; ++y) {
				if (beginingPoints.get(x, y)) {
					beginingPoints.unSet(x, y);
					createLine(x, y);
				}
			}
		}

		return this.lines;
	}

	private void createLine(int x, int y) {
		Point2d p = getNeighbour(new Point2d(x, y));

		if (p == null)
			return;

		Vector2d v = new Vector2d(new Point2d(x, y), p);
		beginingPoints.unSet(p);
		growLine(v);
		lines.add(v);
	}

	private void growLine(Vector2d v) {
		Point2d p = getNeighbour(v.getLast());
		if (p == null)
			return;
		if (check(v, p)) {
			beginingPoints.unSet(p);
			v.add(p);
			growLine(v);
		}
	}

	private Point2d getNeighbour(Point2d p) {
		List<Point2d> neighbours = getNeighbours(p.getXAsInt(), p.getYAsInt());
		if (!neighbours.isEmpty())
			return neighbours.get(0);
		else
			return null;
	}

	private boolean isForeground(int x, int y) {
		for (int i = 0; i < backgroundColor.length; ++i) {
			if (backgroundColor[i] != image.getSample(x, y, i))
				return true;
		}
		return false;
	}

	private List<Point2d> getNeighbours(int x, int y) {
		List<Point2d> neighbours = new ArrayList<Point2d>(4);
		if (x < xLength - 1) {
			if (y > 0 && isForeground(x + 1, y - 1)) {
				neighbours.add(new Point2d(x + 1, y - 1));
			}
			if (isForeground(x + 1, y))
				neighbours.add(new Point2d(x + 1, y));
			if (y < yLength - 1 && isForeground(x + 1, y + 1)) {
				neighbours.add(new Point2d(x + 1, y + 1));
			}
		}
		if (y < yLength - 1 && isForeground(x, y + 1)) {
			neighbours.add(new Point2d(x, y + 1));
		}
		return neighbours;
	}

	private boolean check(Vector2d v, Point2d p) {
		Point2d first = v.getFirst();
		Point2d last = v.getLast();
		List<Point2d> points = v.getPoints();

		if (checkUsingDotProduct) {
			double v1x = last.getX() - first.getX();
			double v1y = last.getY() - first.getY();

			double v2x = p.getX() - last.getX();
			double v2y = p.getY() - last.getY();

			double len = Math.sqrt(v1x * v1x + v1y * v1y);
			v1x /= len;
			v1y /= len;

			len = Math.sqrt(v2x * v2x + v2y * v2y);
			v2x /= len;
			v2y /= len;

			double dotProduct = v1x * v2x + v1y * v2y;
			if (!(dotProduct > acceptableCosinusAngle))
				return false;
		}

		if (checkUsingDistance) {
			//if (Math.abs(first.getX() - p.getX()) < Point2d.DOUBLE_PREC) {
			if (Double.compare(first.getX(), p.getX()) == 0) {
				for (int i = 1; i < points.size(); ++i) {
					if (Math.abs(points.get(i).getX() - p.getX()) > acceptableDistance) {
						return false;
					}
				}
			} else {
				double A, B, C;
				A = p.getY() - first.getY();
				A /= p.getX() - first.getX();
				B = -1;
				C = ((double) first.getY()) - A * (double) first.getX();

				double d;
				for (int i = 1; i < points.size(); ++i) {
					double x = points.get(i).getX();
					double y = points.get(i).getY();
					d = Math.abs(A * x + B * y + C) / Math.sqrt(A * A + B * B);
					if (d > acceptableDistance) {
						return false;
					}
				}
			}
		}
		return true;
	}
}
